feat(plugin-terminals): terminals plugin with readonly + interactive PTY modes#37
Merged
Conversation
✅ Deploy Preview for devfra ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…ive PTY modes Introduce @devframes/plugin-terminals, a portable hub-native terminal panel built on the core devframe RPC + streaming surface (no hard hub dependency). It runs standalone via the CLI, mounts into a Vite host, and docks inside a hub. Two interaction modes: - readonly: a piped child process whose merged output is streamed to viewers; input is rejected. Ideal for dev servers / logs. - interactive: a real PTY (node-pty prebuilt) that accepts keystrokes and resize, so full-screen TUIs (vim, htop, Claude Code) render correctly. Falls back to a piped child process with a diagnostic when no PTY backend is present. Output streams over a per-session channel (one stream kept open for the session's life so restart reuses the same id), session metadata syncs via shared state, and spawning is allow-list gated (presets + opt-in arbitrary commands). Ships node + client + cli + vite entries plus a self-contained xterm SPA, structured DP_TERMINALS diagnostics, and an e2e test suite covering streaming, stdin, TTY detection, and SIGWINCH.
…ith rename - Follow the system color mode and react to changes at runtime: the UI chrome (CSS variables) and every xterm instance switch between dark and a GitHub-light palette without reload, driven by prefers-color-scheme. - Tab labels show the live foreground process of the controlling TTY (e.g. bash → vim → bash), polled from node-pty for PTY sessions. - Custom renaming: double-click a tab to edit inline; backed by a new `devframes-plugin-terminals:rename` RPC. Display precedence is customTitle > processName > base title. Adds processName/customTitle to the session descriptor, a getProcessName hook on the PTY backend, an unref'd poll timer (cleared on exit/restart/ remove/dispose), and tests for process-name tracking and renaming.
…etadata, per-package typecheck) Merge upstream/main and reconcile the plugin with its newer baseline: - resolve to nostics ^1.1.4 via the catalog and regenerate the lockfile (fixes the broken merged lockfile that failed frozen CI installs). - supply the now-required DevframeDefinition metadata (version, packageName, homepage, description) from package.json. - add a `typecheck` script and switch tsconfig to explicit include/exclude (drop composite) so `turbo run typecheck` passes for the package. - refresh tsnapi snapshots for the nostics v1 diagnostics handle shape.
- Gate the PTY-semantics tests (stdin echo, SIGWINCH resize, foreground process name) to POSIX; Windows keeps the isTTY interactive coverage. These rely on behaviours conpty doesn't provide (no SIGWINCH; `.process` returns the TERM name). - Ignore the Windows `xterm-256color` TERM-name fallback so it never surfaces as a session label. - Dispose the terminal manager on test server close so spawned PTY/piped child processes don't leak across runs.
…, + tab - Mirror the active terminal into the URL hash (`#id=<sessionId>`) and react to external hash changes (links, back/forward, manual edits). - Spawn and select a fresh interactive session on every page load. - Move the "new terminal" affordance to a compact "+" pinned at the end of the tab strip. Avoids refocusing the active terminal on background shared-state updates by only fitting/focusing when the selection actually changes.
The prebuilt PTY binary isn't published for every Node ABI on every OS (e.g. Node 26 on Windows 404s and the source-build fallback crashes), which failed `pnpm install`. node-pty is already lazily imported with a piped-child fallback, so move it to optionalDependencies — a missing prebuild is now non-fatal. Gate the PTY tests on actual backend availability: the real-TTY check runs wherever a PTY exists (incl. Windows conpty), while the POSIX-only semantics (SIGWINCH, foreground process name, stdin echo) stay POSIX-gated.
…d of spawning A reload was always starting a new shell: the sessions shared state resolves with its empty initial value and backfills the server's sessions asynchronously, so the autostart check always saw an empty list. Decide autostart from the authoritative `list` RPC and seed the initial render from it, so a refresh restores the persisted sessions (reselecting the URL-hashed one) and only spawns a shell when none exist.
c58b6d9 to
cfe10d0
Compare
Restyle the terminal panel after the antfu / vitejs-devtools (rolldown) aesthetic: - Adopt presetWind4 with semantic tokens (bg-base, bg-secondary, border-base, color-active, op-fade, op-mute) and a green primary accent; light and dark are designed together and driven by the `.dark` class. - Replace ad-hoc inline color classes with the token system and reusable shortcuts (btn-action, btn-icon, tab-item, status dots, mode badges). - Cleaner shell: brand + session tabs with status dots and hover-close, a "+" icon button, a presets dropdown, and a details toolbar with restart / kill icon actions. Phosphor duotone icons throughout. - Add a global stylesheet (base shell, themed scrollbars, xterm host) and tie the xterm themes to the app surface (off-black #111, primary cursor).
There was a problem hiding this comment.
Pull request overview
Adds a new built-in workspace plugin, @devframes/plugin-terminals, implementing a portable terminals panel that can run standalone (CLI/SPA) or mount into a Vite host via the existing devframe RPC + streaming surface.
Changes:
- Introduces
plugins/terminalsworkspace: node-side terminal manager (PTY + piped fallback), RPC surface, client renderer, and SPA. - Wires the new workspace into repo build/test/type infrastructure (tsconfig paths, turbo pipeline, vitest projects, pnpm catalogs/lockfile).
- Adds tsnapi public API snapshots for the new package entrypoints.
Reviewed changes
Copilot reviewed 49 out of 59 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Registers plugins/terminals as a vitest workspace project. |
| turbo.json | Adds Turbo build pipeline entry for @devframes/plugin-terminals. |
| tsconfig.base.json | Adds TS path aliases for @devframes/plugin-terminals/* entrypoints. |
| pnpm-workspace.yaml | Adds catalogs and allowBuilds entry for the optional PTY backend + frontend deps for the plugin SPA/client. |
| pnpm-lock.yaml | Locks new dependency graph for the terminals plugin workspace. |
| alias.ts | Adds runtime/build alias entries for the new plugin entrypoints. |
| plugins/terminals/uno.config.ts | UnoCSS theme + shortcuts for the terminals SPA/client UI. |
| plugins/terminals/tsdown.config.ts | tsdown build configuration for node/neutral builds and combined DTS emission. |
| plugins/terminals/tsconfig.json | Plugin-local TS config (DOM lib enabled; includes tests/build config). |
| plugins/terminals/test/terminals.test.ts | End-to-end RPC + streaming tests for readonly + interactive (PTY) behavior. |
| plugins/terminals/test/_utils.ts | HTTP+WS harness to boot the devframe for tests and collect streaming output. |
| plugins/terminals/src/vite.ts | Vite adapter helper (terminalsVite) built on viteDevBridge. |
| plugins/terminals/src/types.ts | Public types for sessions, presets, spawn requests, and options. |
| plugins/terminals/src/spa/vite.config.ts | Vite config for building the bundled SPA (base: './'). |
| plugins/terminals/src/spa/main.ts | SPA entry mounting the terminals client. |
| plugins/terminals/src/spa/index.html | SPA HTML shell. |
| plugins/terminals/src/rpc/schemas.ts | Valibot schemas for terminal RPC request/response payloads. |
| plugins/terminals/src/rpc/index.ts | RPC function aggregation + devframe module augmentation for server fns/shared-state keys. |
| plugins/terminals/src/rpc/functions/write.ts | RPC action to write input to a session. |
| plugins/terminals/src/rpc/functions/terminate.ts | RPC action to terminate a session’s process. |
| plugins/terminals/src/rpc/functions/spawn.ts | RPC action to spawn a new session. |
| plugins/terminals/src/rpc/functions/restart.ts | RPC action to restart a session in-place. |
| plugins/terminals/src/rpc/functions/resize.ts | RPC action to resize a session. |
| plugins/terminals/src/rpc/functions/rename.ts | RPC action to set/clear a session’s custom title. |
| plugins/terminals/src/rpc/functions/remove.ts | RPC action to remove a session and its resources. |
| plugins/terminals/src/rpc/functions/presets.ts | RPC query to list spawnable presets. |
| plugins/terminals/src/rpc/functions/list.ts | RPC query to list session descriptors. |
| plugins/terminals/src/node/manager.ts | Core node-side session lifecycle manager + streaming + shared-state publishing. |
| plugins/terminals/src/node/index.ts | Node entrypoint: wires manager + registers RPC functions. |
| plugins/terminals/src/node/diagnostics.ts | Plugin diagnostics (DP_TERMINALS_000x) via nostics. |
| plugins/terminals/src/node/context.ts | Context-scoped manager storage (WeakMap). |
| plugins/terminals/src/node/backend.ts | PTY backend loader + pipe fallback implementation. |
| plugins/terminals/src/index.ts | Devframe definition factory + default export; CLI config + SPA dist wiring. |
| plugins/terminals/src/env.d.ts | Svelte/UnoCSS/CSS module typings for TS. |
| plugins/terminals/src/constants.ts | Shared constants (IDs, state keys, defaults). |
| plugins/terminals/src/client/vite.config.ts | Vite build for embeddable client renderer bundle. |
| plugins/terminals/src/client/TerminalView.svelte | Xterm-based terminal view + input/resize + stream subscription. |
| plugins/terminals/src/client/styles.css | Base styles and xterm viewport styling. |
| plugins/terminals/src/client/index.ts | Public mountTerminals() API + exports for consumers. |
| plugins/terminals/src/client/App.svelte | Terminals UI: session tabs, presets menu, toolbar, view switching. |
| plugins/terminals/src/cli.ts | CLI adapter factory createTerminalsCli(). |
| plugins/terminals/package.json | New workspace package manifest + exports + build scripts + deps. |
| plugins/terminals/bin.mjs | Package bin entrypoint for the terminals CLI. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/vite.snapshot.js | tsnapi JS API snapshot for /vite. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/vite.snapshot.d.ts | tsnapi DTS API snapshot for /vite. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/types.snapshot.js | tsnapi JS API snapshot for /types. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/types.snapshot.d.ts | tsnapi DTS API snapshot for /types. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/rpc.snapshot.js | tsnapi JS API snapshot for /rpc. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/rpc.snapshot.d.ts | tsnapi DTS API snapshot for /rpc. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/node.snapshot.js | tsnapi JS API snapshot for /node. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/node.snapshot.d.ts | tsnapi DTS API snapshot for /node. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/index.snapshot.js | tsnapi JS API snapshot for package root. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/index.snapshot.d.ts | tsnapi DTS API snapshot for package root. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/constants.snapshot.js | tsnapi JS API snapshot for /constants. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/constants.snapshot.d.ts | tsnapi DTS API snapshot for /constants. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/client.snapshot.js | tsnapi JS API snapshot for /client. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/client.snapshot.d.ts | tsnapi DTS API snapshot for /client. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/cli.snapshot.js | tsnapi JS API snapshot for /cli. |
| tests/snapshots/tsnapi/@devframes/plugin-terminals/cli.snapshot.d.ts | tsnapi DTS API snapshot for /cli. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+12
to
+13
| cols: v.optional(v.number()), | ||
| rows: v.optional(v.number()), |
| 'types': 'src/types.ts', | ||
| } | ||
|
|
||
| // Three configs: |
Comment on lines
+174
to
+191
| <button | ||
| type="button" | ||
| class="group tab-item {activeId === s.id ? 'tab-item-active' : ''}" | ||
| title={`${displayName(s)} — double-click to rename`} | ||
| onclick={() => (activeId = s.id)} | ||
| ondblclick={(e) => { e.preventDefault(); e.stopPropagation(); renamingId = s.id }} | ||
| > | ||
| <span class="h-1.5 w-1.5 rounded-full shrink-0 {statusDot(s.status)}"></span> | ||
| <span class="truncate">{displayName(s)}</span> | ||
| <span | ||
| role="button" | ||
| tabindex="-1" | ||
| aria-label="Close terminal" | ||
| class="i-ph-x op0 group-hover:op60 hover:op100! transition-opacity shrink-0" | ||
| onclick={(e) => { e.stopPropagation(); rpc.call('devframes-plugin-terminals:remove', { id: s.id }).catch(() => {}) }} | ||
| onkeydown={() => {}} | ||
| ></span> | ||
| </button> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
@devframes/plugin-terminals— the built-in terminals plugin (#2 in the plugin planning index) — as a newplugins/*workspace. It's a portable, hub-native terminal panel built on the core devframe RPC + streaming surface (no hard hub dependency): it runs standalone via the CLI, mounts into a Vite host, and docks inside a hub.Modes
writeis rejected (DP_TERMINALS_0003). Ideal for dev servers / logs.@homebridge/node-pty-prebuilt-multiarch, prebuilt so there's no native compile step) that accepts keystrokes and resize. Degrades to a piped child process with a diagnostic when no PTY backend is available.TUI support (e.g. Claude Code)
Interactive sessions are backed by a genuine pseudo-terminal, verified end-to-end:
process.stdout.isTTY === true),What's included (
plugins/terminals/)TerminalManager(PTY + pipe backends; one stream per session kept open for the session's life sorestartreuses the same id),setupTerminals(ctx), allow-list spawn model (presets+ opt-inallowArbitraryCommands, default-deny), structuredDP_TERMINALS_*diagnostics.defineRpcFunction, namespaceddevframes-plugin-terminals:*):list,presets,spawn,write,resize,terminate,restart,remove, withdeclare module 'devframe'augmentation for server functions + shared-state keys.mountTerminals()xterm.js renderer (CSS inlined, self-contained) — input wired for interactive, disabled for readonly, fit/resize handling. Reusable by the SPA and as a hubcustom-renderentry..(createTerminalsDevframefactory + default export),/cli,/vite, plusbin.mjs. Mount path followsresolveBasePath(/standalone,/__id/hosted).Repo wiring
pnpm-workspace.yaml(plugins/*glob + catalogs for node-pty & xterm + allow-build),turbo.json,alias.ts/tsconfig.base.json,vitest.config.ts.Verification
pnpm lint && pnpm test && pnpm buildall green (379 tests, 38 files). Plugin suite covers readonly streaming + exit, readonly write rejection, interactive PTY stdin, TTY detection, SIGWINCH resize, restart id reuse, list/remove, presets, and arbitrary-command rejection — over a real HTTP + WebSocket harness. tsnapi API snapshots added for every entry.This PR was created with the help of an agent.